#include <stdio.h>
#include <stdarg.h>
#include <sys/time.h>
#include <lua.hpp>
#include <cstdio>
#include <iostream>
#include <gloox/client.h>
#include <gloox/messagehandler.h>
#include <gloox/presencehandler.h>
#include <gloox/disco.h>
#include <gloox/connectionlistener.h>
#include <gloox/messagesessionhandler.h>
#include <gloox/subscriptionhandler.h>

#define DLLEXPORT __attribute__ ((visibility("default")))
#define LUACF(F) int F (lua_State * L)
#define REG(NAME, FUNC) (lua_pushstring(L, NAME), lua_pushcfunction(L, FUNC), lua_settable(L, -3))
#define ASIZE(A) (sizeof(A) / sizeof((A)[0]))


using namespace gloox;
using namespace std;


class Handler : public MessageHandler, public PresenceHandler, public ConnectionListener, public MessageSessionHandler, public SubscriptionHandler {
private:
	lua_State * L;
	void dispatch(const char * type, const char * proto, ...);
public:
	Handler(lua_State * L);
	virtual void handleMessage(Stanza * stanza, MessageSession * session);
	virtual void handlePresence(Stanza * stanza);
	virtual void onConnect();
	virtual void onDisconnect(ConnectionError e);
	virtual bool onTLSConnect(const CertInfo &info);
	virtual void handleMessageSession(MessageSession *session);
	virtual void handleSubscription(Stanza *stanza);
};

class Exception {
private:
	char m_message[1000];
public:
	Exception(const char * format = NULL, ...);
	const char * message();
};


Client * client = NULL;
Handler * handler = NULL;


void error(const char * format, ...){
	va_list ap;
	
	fprintf(stderr, "Jabber error: ");
	va_start(ap, format);
	vfprintf(stderr, format, ap);
	va_end(ap);
	fprintf(stderr, "\n");
	fflush(stderr);
}

Handler::Handler(lua_State * L){
	this->L = L;
}

void Handler::dispatch(const char * type, const char * proto, ...){
	va_list ap;
	int i, result;
	const char * str, * errmsg;
	
	// printf("dispatch(%s)\n", type);
	lua_getglobal(L, "jabber");
	if(lua_isnil(L, -1)) return;
	lua_pushstring(L, "dispatcher");
	lua_gettable(L, -2);
	if(!lua_isfunction(L, -1)) return;
	lua_pushstring(L, type);
	va_start(ap, proto);
	for(i = 0; proto[i]; i++){
		switch(proto[i]){
		case 'S':
			str = va_arg(ap, const char *);
			if(str){
				lua_pushstring(L, str);
			} else {
				lua_pushnil(L);
			}
			break;
		case 'I':
			lua_pushinteger(L, va_arg(ap, int));
			break;
		case 'B':
			lua_pushboolean(L, va_arg(ap, int));
			break;
		default:
			break;
		}
	}
	va_end(ap);
	result = lua_pcall(L, i + 1, 0, 0);
	if(result){
		errmsg = luaL_checkstring(L, -1);
		switch(result){
		case LUA_ERRRUN: error("Runtime error: %s", errmsg); break;
		case LUA_ERRMEM: error("Memory allocation error: %s", errmsg); break;
		case LUA_ERRERR: error("Error while running error handler: %s", errmsg); break;
		}
	}
	lua_pop(L, 1);
}

void Handler::handleMessage(Stanza * stanza, MessageSession * session){
	this->dispatch("message", "SSS", stanza->from().full().c_str(), stanza->subject().c_str(), stanza->body().c_str());
}

void Handler::handlePresence(Stanza * stanza){
	const char * presence;
	
	switch(stanza->presence()){
	case PresenceAvailable: presence = "available"; break;
	case PresenceChat: presence = "chat"; break;
	case PresenceAway: presence = "away"; break;
	case PresenceDnd: presence = "dnd"; break;
	case PresenceXa: presence = "x/a"; break;
	case PresenceUnavailable: presence = "unavailable"; break;
	default: presence = "unknown";
	}
	this->dispatch("presence", "SS", stanza->from().full().c_str(), presence);
}

void Handler::onConnect(){
	this->dispatch("connection", "S", "connect");
}

void Handler::onDisconnect(ConnectionError e){
	this->dispatch("connection", "S", "disconnect");
}

bool Handler::onTLSConnect(const CertInfo &info){
	this->dispatch("connection", "SIBSSIISSSS", "tls",
		info.status,
		info.chain,
		info.issuer.c_str(),
		info.server.c_str(),
		info.date_from,
		info.date_to,
		info.protocol.c_str(),
		info.cipher.c_str(),
		info.mac.c_str(),
		info.compression.c_str()
	);
	return true;
}

void Handler::handleMessageSession(MessageSession *session){
	session->registerMessageHandler(handler);
}

void Handler::handleSubscription(Stanza *stanza){
	const char * subtype;
	
	switch(stanza->subtype()){
	case StanzaS10nSubscribe: subtype = "subscribe"; break;
	case StanzaS10nSubscribed: subtype = "subscribed"; break;
	case StanzaS10nUnsubscribe: subtype = "unsubscribe"; break;
	case StanzaS10nUnsubscribed: subtype = "unsubscribed"; break;
	default: subtype = "unknown";
	}
	this->dispatch("subscription", "SS", stanza->from().full().c_str(), subtype);
}

Exception::Exception(const char * format, ...){
	va_list ap;
	
	if(format){
		va_start(ap, format);
		vsnprintf(this->m_message, ASIZE(this->m_message), format, ap);
		va_end(ap);
	} else {
		snprintf(this->m_message, ASIZE(this->m_message), "<common error>");
	}
}

const char * Exception::message(){
	return (const char *) this->m_message;
}

bool jabber_open(const char * jid, const char * password, const char * server, int port){
	bool connected;
	
	// printf("open(%s, %s, %s, %d)\n", jid, password, server, port);
	if(!handler) throw Exception("Jabber is not initialized");
	if(client) throw Exception("Jabber is already opened");
	client = new Client(JID(jid), password, port);
	client->setServer(server);
	client->registerMessageHandler(handler);
	client->registerPresenceHandler(handler);
	client->registerConnectionListener(handler);
	// client->registerMessageSessionHandler(handler);
//	client->setAutoMessageSession(true, handler);
	client->registerSubscriptionHandler(handler);
	client->disco()->setVersion("LuaJabber", "0.1");
	client->disco()->setIdentity("client", "useragent");
//	client->setAutoPresence(true);
	connected = client->connect(false);
	if(!connected){
		client->disconnect();
		delete(client);
		client = NULL;
	}
	return connected;
}

LUACF(jabber_lua_open){
	const char * jid, * password, * server;
	int port;
	bool isopen = false;
	
	jid = luaL_checkstring(L, 1);
	password = luaL_checkstring(L, 2);
	server = luaL_checkstring(L, 3);
	port = luaL_checkinteger(L, 4);
	try {
		isopen = jabber_open(jid, password, server, port);
	} catch (Exception e){
		return luaL_error(L, e.message());
	}
	lua_pushboolean(L, isopen ? 1 : 0);
	return 1;
}

void jabber_yield(int timeout){
	struct timeval before, after;
	long long diff;
	int epserr = 10000;
	
	if(!client) throw Exception("Jabber is not opened");
	do {
		gettimeofday(&before, NULL);
		client->recv(timeout * 1000);
		gettimeofday(&after, NULL);
		diff = (after.tv_sec - before.tv_sec) * 1000000 + (after.tv_usec - before.tv_usec);
	} while((timeout * 1000) - diff > epserr);
}

LUACF(jabber_lua_yield){
	int timeout;
	
	timeout = luaL_optinteger(L, 1, -1);
	try {
		jabber_yield(timeout);
	} catch (Exception e){
		return luaL_error(L, e.message());
	}
	return 0;
}

void jabber_close(){
	// printf("close()\n");
	if(!client) throw Exception("Jabber is not opened");
	client->disconnect();
	delete(client);
	client = NULL;
}

LUACF(jabber_lua_close){
	try {
		jabber_close();
	} catch (Exception e){
		return luaL_error(L, e.message());
	}
	return 0;
}

void jabber_send(const char * jid, const char * message){
	Stanza * stanza;
	
	// printf("send(%s, %s)\n", jid, message);
	if(!client) throw Exception("Jabber is not opened");
	stanza = Stanza::createMessageStanza(JID(jid), message);
	client->send(stanza);
}

LUACF(jabber_lua_send){
	const char * jid, * message;
	
	jid = luaL_checkstring(L, 1);
	message = luaL_checkstring(L, 2);
	try {
		jabber_send(jid, message);
	} catch (Exception e){
		return luaL_error(L, e.message());
	}
	return 0;
}

bool jabber_isopen(){
	return client != NULL;
}

LUACF(jabber_lua_isopen){
	bool isopen = jabber_isopen();
	lua_pushboolean(L, isopen);
	return 1;
}

extern "C" DLLEXPORT LUACF(luaopen_jabber){
	handler = new Handler(L);
	lua_newtable(L);
	lua_pushvalue(L, -1);
	lua_setglobal(L, "jabber");
	REG("open", jabber_lua_open);
	REG("yield", jabber_lua_yield);
	REG("close", jabber_lua_close);
	REG("send", jabber_lua_send);
	REG("isopen", jabber_lua_isopen);
	return 1;
}


